package com.google.code.sysarch.v4.graphical;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import com.google.code.sysarch.v4.network.*;

import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.openal.AL;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.opengl.glu.GLU;

import com.google.code.sysarch.v4.main.BattleShips;

/**
 * An implementation of GameWindow that will use OPENGL (LWJGL) to 
 * render the scene.
 * 
 * @author Kevin Glass
 * @author Brian Matzon
 * @author Alexander Wirth
 */
public class LWJGLGameWindow {
  
	/** The callback which should be notified of window events */
	private Game callback;
  
	/** True if the game is currently "running", i.e. the game loop is looping */
	private boolean gameRunning = true;
  
	/** The width of the game display area */
	private int width;
  
	/** The height of the game display area */
	private int height;
	
	/** Fullscreen mode ? */
	private boolean fullscreen = false;
	
	/** Multisample value */
	private int multisample;
	
	/** Far Z-plane */
	private float farsight;
  
	/** Title of window */
	private String title;
	
	/** True if the cursor is invisible */
	private boolean cursorgrabbed;
	
	/** Timer for framerate-independent updating */
	private long timer = 0;
	
	/** Last time fps were updated */ 
	private long fps_last;
	
	/** Frames that were rendered since last fps update */
	private int fps_counter = 0;
	
	/** The actual fps */
	private int fps;	
	
	/**
	 * Create a new game window that will use OpenGL to 
	 * render our game.
	 */
	public LWJGLGameWindow() {
		farsight = 100;
	}
	
	/**
	 * Set the title of this window.
	 *
	 * @param title The title to set on this window
	 */
	public void setTitle(String title) {
	    this.title = title;
	    if(Display.isCreated()) {
	    	Display.setTitle(this.title);
	    }
	}

	/**
	 * Set the resolution of the game display area.
	 *
	 * @param x The width of the game display area
	 * @param y The height of the game display area
	 */
	public void setResolution(int x, int y) {
		this.width = x;
		this.height = y;
	}
	
	/**
	 * Sets whether to switch to fullscreen mode
	 * @param fs True if fullscreen mode should be used
	 */
	public void setFullscreen(boolean fs) {
		this.fullscreen = fs;
	}
	
	public void setMultisample(int m) {
		if (m > 8) m = 8;
		if (m < 0) m = 0;
		if (m == 3) m = 4;
		if (m == 5) m = 4;
		if (m == 6) m = 4;
		if (m == 7) m = 8;
		this.multisample = m;
	}
	
	/**
	 * Register a callback that will be notified of game window
	 * events.
	 *
	 * @param callback The callback that should be notified of game
	 * window events. 
	 */
	public void setGameWindowCallback(Game callback) {
		this.callback = callback;
	}	
	
	/**
	 * Sets the maximum viewing/drawing distance
	 */
	public void setFarSight(float distance) {
		this.farsight = distance;
	}
	
	/**
	 * @return Returns the maximum viewing/drawing distance
	 */
	public float getFarSight() {
		return this.farsight;
	}
	
	/**
	 * @return Returns the width of the window
	 */
	public int getWidth() {
		return this.width;
	}
	
	/**
	 * @return Returns the height of the window
	 */	
	public int getHeight() {
		return this.height;
	}
	
	/**
	 * @return Returns the current frames per second
	 */	
	public int getFPS() {
		return this.fps;
	}
	
	public void setCursorGrabbed(boolean on) {
		this.cursorgrabbed = on;
	}
	
	/**
	 * Sets the display mode
	 */
	private boolean setDisplayMode() {
		try {
			// get modes
			DisplayMode[] dm = org.lwjgl.util.Display.getAvailableDisplayModes(
					width, height, -1, -1, -1, -1, 60, 60);

			org.lwjgl.util.Display.setDisplayMode(dm, new String[] {
					"width=" + width,
					"height=" + height,
					"freq=" + 60,
					"bpp="
							+ org.lwjgl.opengl.Display.getDisplayMode()
									.getBitsPerPixel() });
			Display.setVSyncEnabled(true);
			Display.setFullscreen(fullscreen);
			PixelFormat pix = new PixelFormat(org.lwjgl.opengl.Display.getDisplayMode().getBitsPerPixel(),
				     8,   // alpha
				    24,   // depth buffer
				     1,   // stencil buffer
				     multisample) ; // multisample
			if (BattleShips.optionWnd.useCustomPixelFormat()) {
				Display.create(pix);
			} else {
				Display.create();
			}
			Keyboard.create();
			Mouse.create();
			if (BattleShips.optionWnd.isSoundEnabled() || BattleShips.optionWnd.isMusicEnabled()) {
				AL.create(null, 15, 22050, true);
			}

			return true;
		} catch (LWJGLException e) {
			e.printStackTrace();
			callback.windowClosed();
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("An error occurred during initialisation.");
			System.exit(1);
		}

		return false;
	}
	
	/**
	 * Create a window and initialize all OpenGL parameters used for rendering
	 */
	public void initRendering() {            
		setDisplayMode();
		
	// Smooth Shading
		GL11.glShadeModel(GL11.GL_SMOOTH);
	// Texture2D
		GL11.glEnable(GL11.GL_TEXTURE_2D);
	// Normalisierung der Normalenvektoren
		GL11.glEnable(GL11.GL_NORMALIZE);
	// Blending
		GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
		//GL11.glEnable(GL11.GL_BLEND);
	// Background color
		GL11.glClearColor(0, 0, 0, 0);
	// Depth Test
		GL11.glEnable(GL11.GL_DEPTH_TEST);
		GL11.glClearDepth(1.0f);
		GL11.glDepthFunc(GL11.GL_LESS);
	// Perspective Correction
		GL11.glHint(GL11.GL_PERSPECTIVE_CORRECTION_HINT, GL11.GL_NICEST);				
	// Lighting
		float[] LightAmbient = { 0.25f, 0.25f, 0.25f, 1.0f };
		float[] LightDiffuse = { 1.0f, 1.0f, 1.0f, 1.0f };
		float[] LightSpecular = { 1.0f, 1.0f, 1.0f, 1.0f };
		GL11.glLight(GL11.GL_LIGHT0, GL11.GL_AMBIENT, toFloatBuffer(LightAmbient));
		GL11.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, toFloatBuffer(LightDiffuse));
		GL11.glLight(GL11.GL_LIGHT0, GL11.GL_SPECULAR, toFloatBuffer(LightSpecular));
		GL11.glEnable(GL11.GL_LIGHT0);
		GL11.glEnable(GL11.GL_LIGHTING);
		GL11.glEnable(GL11.GL_COLOR_MATERIAL);
	// Backface Culling
		//GL11.glEnable(GL11.GL_CULL_FACE);
	// Clear the stencil buffer to 0
		GL11.glClearStencil(0);
	// Disable Dithering
		GL11.glDisable(GL11.GL_DITHER);
		
		GL11.glViewport(0, 0, this.width, this.height);
		GL11.glMatrixMode(GL11.GL_PROJECTION);
		GL11.glLoadIdentity();
		GLU.gluPerspective(45.0f, (float) this.width / (float) this.height, 0.1f, farsight);
		GL11.glMatrixMode(GL11.GL_MODELVIEW);
		GL11.glLoadIdentity();
	}
  
	/**
	 * Run the main game loop. This method keeps rendering the scene
	 * and requesting that the callback update its screen.
	 */
	public void gameLoop() {
		// Grab the mouse if neccessary
		Mouse.setGrabbed(cursorgrabbed);		
		// Reset the timer
		this.timer = System.nanoTime();
		
		while (gameRunning) {
			
			//Calculate the time passed since the last loop
			double delta = (double)(System.nanoTime() - this.timer) / 1000000000L;
			this.timer = System.nanoTime();
			calculateFPS();
			
			// let subsystem paint
			if (callback != null) {
				callback.frameRendering(delta);
			}
			
			if(Display.isCloseRequested() || Keyboard.isKeyDown(Keyboard.KEY_ESCAPE)) {
				quitGame();
			}
		}
	}  
	
	
	public void quitGame() {
		gameRunning = false;
		BattleShips.network.send(NetworkCommands.QUIT);
		System.out.println("LWJGLGAMEWINDOW: Quitting.");
		Display.destroy();
		callback.windowClosed();
	}
	
	/** 
	 * Calculates the PFS.
	 * Must be called in every rendering loop, to work correct 
	 */
	private void calculateFPS() {
		this.fps_counter++;
		if (System.nanoTime() - this.fps_last >= 5000000000L) {
			// Update every 5 seconds
			this.fps = this.fps_counter/5;
			this.fps_counter = 0;
			this.fps_last = System.nanoTime();
			System.out.println("LWJGLGAMEWINDOW: FPS: "+this.fps);
		}
	}
		
	
	public static FloatBuffer toFloatBuffer(float data[]) {
		FloatBuffer buffer = ByteBuffer.allocateDirect(
		data.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
		buffer.clear();
		buffer.put(data);
		buffer.rewind();
		return buffer;
	}
	
	public static DoubleBuffer toDoubleBuffer(double data[]) {
		DoubleBuffer buffer = ByteBuffer.allocateDirect(
		data.length * 8).order(ByteOrder.nativeOrder()).asDoubleBuffer();
		buffer.clear();
		buffer.put(data);
		buffer.rewind();
		return buffer;
	}	
}